package org.gephi.visualization.swing;

import com.sun.opengl.util.BufferUtil;
import java.awt.Color;
import java.nio.DoubleBuffer;
import java.nio.IntBuffer;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.glu.GLU;
import org.gephi.visualization.VizController;
import org.gephi.visualization.config.GraphicalConfiguration;
import org.gephi.visualization.opengl.Lighting;
import org.gephi.visualization.screenshot.ScreenshotMaker;

/**
 *
 * @author Mathieu Bastian
 */
public abstract class GLAbstractListener implements GLEventListener {

    protected GLAutoDrawable drawable;
    protected VizController vizController;
    public static final GLU glu = new GLU();
    private static boolean DEBUG = true;
    private long startTime = 0;
    protected float fps;
    protected float fpsAvg = 0;
    protected float fpsCount = 0;
    private volatile boolean resizing = false;
    public final float viewField = 30.0f;
    public final float nearDistance = 1.0f;
    public final float farDistance = 100000f;
    private double aspectRatio = 0;
    protected DoubleBuffer projMatrix = BufferUtil.newDoubleBuffer(16);
    protected DoubleBuffer modelMatrix = BufferUtil.newDoubleBuffer(16);
    protected IntBuffer viewport = BufferUtil.newIntBuffer(4);
    protected GraphicalConfiguration graphicalConfiguration;
    protected Lighting lighting = new Lighting();
    protected ScreenshotMaker screenshotMaker;

    protected void initDrawable(GLAutoDrawable drawable) {
        this.drawable = drawable;
        drawable.addGLEventListener(this);
    }

    protected abstract void init(GL gl);

    protected abstract void render3DScene(GL gl, GLU glu);

    protected abstract void reshape3DScene(GL gl);

    protected abstract void setCameraPosition(GL gl, GLU glu);

    protected GLCapabilities getCaps() {
        GLCapabilities caps = new GLCapabilities();
        caps.setAlphaBits(8);		//if NOT opaque
        caps.setDoubleBuffered(true);
        caps.setHardwareAccelerated(true);

        //FSAA
        int antialisaing = vizController.getVizConfig().getAntialiasing();
        if (antialisaing == 0) {
            caps.setSampleBuffers(false);
        } else if (antialisaing == 2) {
            caps.setSampleBuffers(true);
            caps.setNumSamples(2);
        } else if (antialisaing == 4) {
            caps.setSampleBuffers(true);
            caps.setNumSamples(4);
        } else if (antialisaing == 8) {
            caps.setSampleBuffers(true);
            caps.setNumSamples(8);
        } else if (antialisaing == 16) {
            caps.setSampleBuffers(true);
            caps.setNumSamples(16);
        }

        return caps;
    }

    public void initConfig(GL gl) {
        //Disable Vertical synchro
        gl.setSwapInterval(0);

        //Depth
        if (vizController.getVizModel().isUse3d()) {
            gl.glEnable(GL.GL_DEPTH_TEST);      //Enable Z-Ordering
            gl.glDepthFunc(GL.GL_LEQUAL);
            gl.glHint(GL.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST);	//Correct texture & colors perspective calculations
        } else {
            gl.glDisable(GL.GL_DEPTH_TEST);     //Z is set by the order of drawing
        }

        //Cull face
        if (vizController.getVizModel().isCulling()) {        //When enabled, increases performance but polygons must be drawn counterclockwise
            gl.glEnable(GL.GL_CULL_FACE);
            gl.glCullFace(GL.GL_BACK);      //Hide back face of polygons
        }

        //Point Smooth
        if (vizController.getVizConfig().isPointSmooth()) {        //Only for GL_POINTS
            gl.glEnable(GL.GL_POINT_SMOOTH);
            gl.glHint(GL.GL_POINT_SMOOTH_HINT, GL.GL_NICEST); //Point smoothing
        } else {
            gl.glDisable(GL.GL_POINT_SMOOTH);
        }

        //Light Smooth
        if (vizController.getVizConfig().isLineSmooth()) {         //Only for GL_LINES
            gl.glEnable(GL.GL_LINE_SMOOTH);
            if (vizController.getVizConfig().isLineSmoothNicest()) {
                gl.glHint(GL.GL_LINE_SMOOTH_HINT, GL.GL_NICEST);
            } else {
                gl.glHint(GL.GL_LINE_SMOOTH_HINT, GL.GL_FASTEST);
            }
        } else {
            gl.glDisable(GL.GL_LINE_SMOOTH);
        }

        gl.glClearDepth(1.0f);

        //Background
        Color backgroundColor = vizController.getVizModel().getBackgroundColor();
        gl.glClearColor(backgroundColor.getRed() / 255f, backgroundColor.getGreen() / 255f, backgroundColor.getBlue() / 255f, 1f);

        //Lighting
        if (vizController.getVizModel().isLighting()) {
            gl.glEnable(GL.GL_LIGHTING);
            setLighting(gl);
            gl.glEnable(GL.GL_NORMALIZE);       //Normalise colors when glScale used
            gl.glShadeModel(GL.GL_SMOOTH);
        } else {
            gl.glDisable(GL.GL_LIGHTING);
            gl.glShadeModel(GL.GL_FLAT);
        }

        //Blending
        if (vizController.getVizConfig().isBlending()) {
            gl.glEnable(GL.GL_BLEND);
            if (vizController.getVizConfig().isBlendCinema()) {
                gl.glBlendFunc(GL.GL_CONSTANT_COLOR, GL.GL_ONE_MINUS_SRC_ALPHA);        //Black display
            } else {
                gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);             //Use alpha values correctly
            }
        }


        //Material
        if (vizController.getVizModel().isMaterial()) {
            gl.glColorMaterial(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE);
            gl.glEnable(GL.GL_COLOR_MATERIAL);                                      //Use color and avoid using glMaterial
        }
        //Mesh view
        if (vizController.getVizConfig().isWireFrame()) {
            gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE);
        }

        gl.glEnable(GL.GL_TEXTURE_2D);

    }

    protected void setLighting(GL gl) {
        lighting = new Lighting();
        lighting.glInit(gl);
    }

    @Override
    public void init(GLAutoDrawable drawable) {
        GL gl = drawable.getGL();

        graphicalConfiguration = new GraphicalConfiguration();
        graphicalConfiguration.checkGeneralCompatibility(gl);

        //Reinit viewport, to ensure reshape to perform
        viewport = BufferUtil.newIntBuffer(4);

        resizing = false;
        initConfig(gl);
        init(gl);
    }

    @Override
    public void display(GLAutoDrawable drawable) {

        //Screenshot
        screenshotMaker.openglSignal(drawable);

        //FPS
        if (startTime == 0) {
            startTime = System.currentTimeMillis() - 1;
        }
        long endTime = System.currentTimeMillis();
        long delta = endTime - startTime;
        startTime = endTime;
        fps = 1000.0f / delta;
        if (fps < 100) {
            fpsAvg = (fpsAvg * fpsCount + fps) / ++fpsCount;
        } 

        GL gl = drawable.getGL();

        if (vizController.getVizModel().isUse3d()) {
            gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
        } else {
            gl.glClear(GL.GL_COLOR_BUFFER_BIT);
        }

        render3DScene(gl, glu);
    }

    public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
        if (!resizing) {
            if (viewport.get(2) == width && viewport.get(3) == height)//NO need
            {
                return;
            }
            resizing = true;

            if (height == 0) {
                height = 1;
            }
            if (width == 0) {
                width = 1;
            }

            int viewportW = 0, viewportH = 0, viewportX = width, viewportY = height;

            aspectRatio = (double) width / (double) height;
            viewportH = height;
            viewportW = (int) (height * aspectRatio);
            if (viewportW > width) {
                viewportW = width;
                viewportH = (int) (width * (1 / aspectRatio));
            }
            viewportX = ((width - viewportW) / 2);
            viewportY = ((height - viewportH) / 2);

            GL gl = drawable.getGL();

            gl.glViewport(viewportX, viewportY, viewportW, viewportH);
            gl.glGetIntegerv(GL.GL_VIEWPORT, viewport);//Update viewport buffer

            gl.glMatrixMode(GL.GL_PROJECTION);
            gl.glLoadIdentity();
            glu.gluPerspective(viewField, aspectRatio, nearDistance, farDistance);
            gl.glGetDoublev(GL.GL_PROJECTION_MATRIX, projMatrix);//Update projection buffer


            gl.glMatrixMode(GL.GL_MODELVIEW);
            gl.glLoadIdentity();

            reshape3DScene(drawable.getGL());

            if (DEBUG) {
                DEBUG = false;
                System.err.println("GL_VENDOR: " + gl.glGetString(GL.GL_VENDOR));
                System.err.println("GL_RENDERER: " + gl.glGetString(GL.GL_RENDERER));
                System.err.println("GL_VERSION: " + gl.glGetString(GL.GL_VERSION));
            }

            resizing = false;
        }
    }

    public GL getGL() {
        return drawable.getGL();
    }

    @Override
    public void displayChanged(GLAutoDrawable arg0, boolean arg1, boolean arg2) {
    }

    public void setVizController(VizController vizController) {
        this.vizController = vizController;
    }

    public GLAutoDrawable getGLAutoDrawable() {
        return drawable;
    }

    public Lighting getLighting() {
        return lighting;
    }

    public GraphicalConfiguration getGraphicalConfiguration() {
        return graphicalConfiguration;
    }

    protected void resetFpsAverage() {
        fpsAvg = 0;
        fpsCount = 0;
    }

    protected float getFpsAverage() {
        return fpsAvg;
    }
}
